package com.slunk.dodgetrain;

//libgdx specific classes
import java.util.Timer;
import java.util.TimerTask;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.Contact;
import com.badlogic.gdx.physics.box2d.ContactImpulse;
import com.badlogic.gdx.physics.box2d.ContactListener;
import com.badlogic.gdx.physics.box2d.Manifold;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;
import com.badlogic.gdx.scenes.scene2d.actions.*;
import com.badlogic.gdx.scenes.scene2d.ui.Label;

//Self owned packages
import com.slunk.physics.SingleScreenFactory;
import com.slunk.physics.characters.AI_Person;
import com.slunk.physics.characters.PointBoostPowerup;
import com.slunk.physics.characters.Powerup;
import com.slunk.physics.characters.Character;
import com.slunk.physics.characters.SpeedBoostPowerup;
import com.slunk.dodgetrain.CrowdManager;
import com.slunk.dodgetrain.TrainYard;
import com.slunk.dodgetrain.PowerupManager;
import com.slunk.physics.characters.Train;
import com.slunk.utilities.Movement;
import com.slunk.utilities.EntityCategories;
import com.slunk.utilities.ScoreLabel;
//import com.slunk.physics.characters.Powerup;


public class DodgeTrain implements ApplicationListener, InputProcessor, ContactListener {
	//sounds and stuff
	private Sound trainSound;
	private Sound explosionSound;
	private Sound hitSound;
	private Sound powerupSound;
	private Sound pickupSound;
	private Music backgroundMusic;
	
	private Timer timer;
	private int schedule;
	private static int MIN_SCHEDULE = 1000;
	private static int MAX_SCHEDULE = 43000;
	
	boolean isGameOver = false;
	
	//movement bools
	boolean isLeftDepressed = false;
	boolean isRightDepressed = false;
	boolean isDownDepressed = false;
	boolean isUpDepressed = false;
	int touchSemaphore = 0;
	int touchCount = 2;
	
	private OrthographicCamera camera;
	private SpriteBatch batch;
	
	//score rates
	static final int savedPersonEareds = 25;
	static final int deadPersonLost = 5;
	static final int playerDeathLost = 20;
	
	//conversion rates
	static final float WORLD_TO_BOX = 0.01f;
	static final float BOX_WORLD_TO = 100f;
	
	//world step
	static final float BOX_STEP=1/120f;
	static final int BOX_VELOCITY_ITERATIONS = 8;
	static final int BOX_POSITION_ITERATIONS = 3;
	float accumulator;
	
	//Box2d variables
	private Box2DDebugRenderer debugger;	
	private World world;
	
	//screen boundaries
	private SingleScreenFactory boundaries;
	private Character character;
	
	//the train yard
	private TrainYard trainYard;
	
	//the crowd manager
	private CrowdManager crowdManager;
	
	//the powerup manager
	private PowerupManager powerManager;
	
	//array to kill the bodies!
	public Array<Body> destroyed_bodies;
	
	//stage needed to draw the labels
	public Stage stage;
	public ScoreLabel scoreManager;
	
	private Sprite backgroundSprite;
	
	private void createCharacter () {
		if (this.character == null) {
			this.character = new Character(this.world, new Vector2(this.camera.viewportWidth / 2,this.camera.viewportHeight/2),15.0f,9.0f, EntityCategories.CAR.getEntityValue(), (short)(EntityCategories.BOUNDARY.getEntityValue() | EntityCategories.PERSON.getEntityValue() | EntityCategories.TRAIN.getEntityValue()) );
			this.character.setScreenSize(this.camera.viewportWidth, this.camera.viewportHeight);
			this.character.setSprite("data/Car.png");
			this.character.getCharacterBody().setUserData(this.character);
		}
	}
	
	@Override
	public void create() {	
		Gdx.input.setInputProcessor(this);
		
		
		this.schedule = MathUtils.random(MIN_SCHEDULE,MAX_SCHEDULE);
		timer = new Timer();
        timer.schedule(new TrainSoundTask(), this.schedule);
		
		this.camera = new OrthographicCamera();  
		this.camera.viewportHeight = Gdx.graphics.getHeight();
		this.camera.viewportWidth = Gdx.graphics.getWidth();
        this.camera.position.set(camera.viewportWidth * .5f, camera.viewportHeight * .5f, 0f);  
        this.camera.update();  
        
		this.batch = new SpriteBatch();	
		Texture.setEnforcePotImages(false);
		
		//this should always be the first thing to set
		this.backgroundSprite = new Sprite(new Texture (Gdx.files.internal("data/Background.png")));
		
		this.debugger = new Box2DDebugRenderer();
		
		//vector2d for gravity and boolean doSleep
		this.world = new World(new Vector2(0.0f,0.0f), false);
		this.world.setContactListener(this);
		
		//world, width, height, thickness
		this.boundaries = new SingleScreenFactory(this.world, Gdx.graphics.getWidth(),Gdx.graphics.getHeight(),0.0f);

		this.createCharacter();
		
		//create the train yard manager
		this.trainYard = new TrainYard(this.world,this.camera.viewportWidth,this.camera.viewportHeight);
		
		//create the crowd manager
		this.crowdManager = new CrowdManager(this.world,this.camera.viewportWidth,this.camera.viewportHeight,20);
		
		//create the powerup manager
		this.powerManager = new PowerupManager(this.world,this.camera.viewportWidth,this.camera.viewportHeight);
		//this.powerManager.dispatchNewPowerup();
		
		this.destroyed_bodies = new Array<Body>();
		
		this.stage = new Stage(0,0,true);
		this.stage.setCamera(this.camera);
		
		this.scoreManager = new ScoreLabel("Score : ", new LabelStyle(new BitmapFont(), Color.WHITE));
		this.scoreManager.x = this.camera.viewportWidth - 100;
		this.scoreManager.y = this.scoreManager.height + 5;
		this.stage.addActor(this.scoreManager);
		
		//initiate sounds/music
		trainSound = Gdx.audio.newSound((Gdx.files.internal("data/sfx/train_sfx.mp3")));
		explosionSound = Gdx.audio.newSound((Gdx.files.internal("data/sfx/Explosion27.wav")));
		hitSound = Gdx.audio.newSound((Gdx.files.internal("data/sfx/Hit_Hurt28.wav")));
		powerupSound = Gdx.audio.newSound((Gdx.files.internal("data/sfx/Powerup23.wav")));
		pickupSound = Gdx.audio.newSound((Gdx.files.internal("data/sfx/Pickup_Coin11.wav")));
		backgroundMusic = Gdx.audio.newMusic(Gdx.files.internal("data/sfx/Amazing_Plan.mp3"));
		backgroundMusic.setLooping(true);
		backgroundMusic.play();
	}

	
	//Timer class to randomly keep changing the movement
		class TrainSoundTask extends TimerTask {
	        public void run() {
	        	trainSound.play();
	    		schedule = MathUtils.random(MIN_SCHEDULE,MAX_SCHEDULE);
	    		timer.schedule(new TrainSoundTask(), schedule);
	        }
	    }
	
	@Override
	public void dispose() {
		this.batch.dispose();
		this.stage.dispose();
	}

	@Override
	public void render() {		
		Gdx.gl.glClearColor(0, 0, 0, 0);
		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
		
		//System.out.println("before world.step");
		this.world.step(BOX_STEP,BOX_VELOCITY_ITERATIONS,BOX_POSITION_ITERATIONS);
		//System.out.println("after world.step");
		//this.debugger.render(this.world,this.camera.combined);
		
		if (this.character != null) {
			setPlayerCharacterMovement();
			this.character.updateMovement();
		}
		
		//update the movement for our trains
		this.trainYard.updateTrainMovements();
		
		//If the train yard is ready, depart a train now after the world has stepped
		if (this.trainYard.isReadyToDepartTrain()) {
			this.trainYard.addTrain();
		}
		
		//Update the movement for our crowds
		this.crowdManager.updateCrowdMovement();
		
		//If crowd manager is ready to birth a new person, best time is now after world has stepped
		if (this.crowdManager.isReadyForBirth()) {
			this.crowdManager.birthPerson();
		}
		
		//need to destroy bodies after-words, might crash otherwise.
		for (Body body: destroyed_bodies) {
			
			//System.out.println(body);
			if (!this.world.isLocked()) {
				if (body.getUserData() != null) {
					if (body.getUserData().getClass() == AI_Person.class) {
						//System.out.println("removing person");
						this.crowdManager.removeFromCrowd((AI_Person)body.getUserData());
						this.destroyed_bodies.removeValue(body, true);
						
						this.world.destroyBody(body);
						body.setUserData(null);
						body = null;
						
					} else if (body.getUserData().getClass() == Character.class){
						this.destroyed_bodies.removeValue(body, true);
						this.world.destroyBody(body);
						body.setUserData(null);
						body = null;
						
						this.character = null;
					} else if (body.getUserData() instanceof Powerup){
						this.powerManager.removePowerup((Powerup)body.getUserData());
						this.destroyed_bodies.removeValue(body, true);
						
						this.world.destroyBody(body);
						body.setUserData(null);
						body = null;
					}
				} 
			}
		}
		
		
		Array<Train> trainsCopy = new Array<Train> (trainYard.getTrains());
		for (Train train : trainsCopy) {
			if (train.getCharacterBody().getUserData() != null) {
				if (train.getCharacterBody().getUserData() instanceof Boolean) {
					Boolean shouldDestroy = (Boolean)train.getCharacterBody().getUserData();
					if (shouldDestroy.booleanValue()) {
						if (!this.world.isLocked()) {
							this.trainYard.removeTrain(train);
							this.world.destroyBody(train.getCharacterBody());
						}
					}
				}
			}
		}
		
		if (this.scoreManager.getWinStreak() >= 100) {
			this.trainYard.increaseDifficulty();
			this.crowdManager.increaseBabyOutput();
			this.powerManager.dispatchNewPowerup();
			this.scoreManager.resetStreaks();
		}
	
		if (this.scoreManager.getLostStreak() >= 100) {
			this.trainYard.decreaseDifficulty();
			this.crowdManager.decreaseBabyOutput();
			this.scoreManager.resetStreaks();
		}
		
		if (this.isGameOver) {
			this.trainYard.removeAllTrains();
			this.trainYard.resetDifficulty();
			this.crowdManager.removeAllPeople();
			this.crowdManager.resetBirthRate();
			this.scoreManager.resetStreaks();
			this.scoreManager.setScoreTo(0);
			this.isGameOver = false;
		}
		
		//Do all the drawing here
		this.batch.begin();
			this.backgroundSprite.draw(this.batch);
			if (this.character != null) {
				this.character.drawSprite(this.batch);
			}
			this.crowdManager.drawPeoples(this.batch);
			this.trainYard.drawTrains(this.batch);
			this.powerManager.drawPowerups(this.batch);
		this.batch.end();
		this.stage.draw();
		this.stage.act(Gdx.graphics.getDeltaTime());
		
		//this.debugger.render(this.world,this.camera.combined);
	}

	@Override
	public void resize(int width, int height) {
	}

	@Override
	public void pause() {
	}

	@Override
	public void resume() {
	}
	
	@Override
	public boolean keyDown(int keycode) {
		if (this.touchSemaphore >= this.touchCount) {
			return false;
		}
		
		switch(keycode) {
		case 19: {
			this.isUpDepressed = true;
			this.touchSemaphore++;
			break;
		}
		case 20: {
			this.isDownDepressed = true;
			this.touchSemaphore++;
			break;
		}
		case 21: {
			this.isLeftDepressed = true;
			this.touchSemaphore++;
			break;
		}
		case 22: {
			this.isRightDepressed = true;
			this.touchSemaphore++;
			break;
		}
		case 62: {
			this.createCharacter();
		}
		default: {
			break;
		}
		}
		return false;
	}

	@Override
	public boolean keyUp(int keycode) {
		// TODO Auto-generated method stub
		
		//19 : up
		//20 : down
		//21 : left
		//22 : right
		
		switch(keycode) {
		case 19: {
			this.isUpDepressed = false;
			this.touchSemaphore--;
			break;
		}
		case 20: {
			this.isDownDepressed = false;
			this.touchSemaphore--;
			break;
		}
		case 21: {
			this.isLeftDepressed = false;
			this.touchSemaphore--;
			break;
		}
		case 22: {
			this.isRightDepressed = false;
			this.touchSemaphore--;
			break;
		}
		default: {
			break;
		}
		}
		
		if (this.touchSemaphore < 0) {
			this.touchSemaphore = 0;
		}
		return false;
	}
	
	private void setPlayerCharacterMovement() {
		if (this.character != null) {
			if (this.touchSemaphore == 0) {
				this.character.setMovementState(Movement.STILL);
			} else if (this.touchSemaphore == 1) {
				if (this.isDownDepressed) {
					//south pressed
					this.character.setMovementState(Movement.SOUTH);
				} else if (this.isLeftDepressed) {
					//west
					this.character.setMovementState(Movement.WEST);
				} else if (this.isRightDepressed) {
					//east
					this.character.setMovementState(Movement.EAST);
				} else if (this.isUpDepressed) {
					this.character.setMovementState(Movement.NORTH);
				}
			} else if (this.touchSemaphore >= 2) {
				if (this.isDownDepressed && this.isLeftDepressed) {
					//south west
					this.character.setMovementState(Movement.SOUTHWEST);
				} else if (this.isDownDepressed && this.isRightDepressed) {
					//south east
					this.character.setMovementState(Movement.SOUTHEAST);
				} else if (this.isUpDepressed && this.isLeftDepressed) {
					//northwest
					this.character.setMovementState(Movement.NORTHWEST);
				} else if (this.isUpDepressed && this.isRightDepressed) {
					//northeast
					this.character.setMovementState(Movement.NORTHEAST);
				}
			}
		}
	}
	
	@Override
	public boolean keyTyped(char character) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean touchDown(int x, int y, int pointer, int button) {
		// TODO Auto-generated method stub
		//System.out.println("touchDown!!");
		return false;
	}

	@Override
	public boolean touchUp(int x, int y, int pointer, int button) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean touchDragged(int x, int y, int pointer) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean touchMoved(int x, int y) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean scrolled(int amount) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public void beginContact(Contact contact) {
		// TODO Auto-generated method stub
		//System.out.println("beginContact");
	}

	@Override
	public void endContact(Contact contact) {
		// TODO Auto-generated method stub
		//System.out.println("endContact");
	}

	//Just to stop the linear velocity so the powerup does not affect the movement of the car
	@Override
	public void preSolve(Contact contact, Manifold oldManifold) {
		// TODO Auto-generated method stub
		Body bodyA = contact.getFixtureA().getBody();
		Body bodyB = contact.getFixtureB().getBody();
		
		if (this.character != null) {
			if (bodyA == this.character.getCharacterBody()) {
				if (this.powerManager.isBodyPowerup(bodyB)) {
					bodyB.setLinearVelocity(0.0f,0.0f);
				}
			} else if (bodyB == this.character.getCharacterBody()) {
				if (this.powerManager.isBodyPowerup(bodyA)) {
					bodyA.setLinearVelocity(0.0f,0.0f);
				}
			}
		}
	}

	@Override
	public void postSolve(Contact contact, ContactImpulse impulse) {
		Body bodyA = contact.getFixtureA().getBody();
		Body bodyB = contact.getFixtureB().getBody();
		
		
		if(this.character != null) {
			boolean foundCollision = false;
			float x = 0.0f;
			float y = 0.0f;
			
			if (bodyA == this.character.getCharacterBody()) {
				if (this.crowdManager.isBodyInCrowds(bodyB) && !this.destroyed_bodies.contains(bodyB, true)) {
					this.destroyed_bodies.add(bodyB);
					foundCollision = true;
					x = bodyB.getPosition().x;
					y = bodyB.getPosition().y;
				} else if (this.powerManager.isBodyPowerup(bodyB) && !this.destroyed_bodies.contains(bodyB, true)) {
					this.playerFoundPowerup(bodyB.getUserData());
					this.destroyed_bodies.add(bodyB);
				}
			} else if (bodyB == this.character.getCharacterBody()) {
				if (this.crowdManager.isBodyInCrowds(bodyA) && !this.destroyed_bodies.contains(bodyA, true)) {
					this.destroyed_bodies.add(bodyA);
					foundCollision = true;
					x = bodyA.getPosition().x;
					y = bodyA.getPosition().y;
				} else if (this.powerManager.isBodyPowerup(bodyA) && !this.destroyed_bodies.contains(bodyA, true)) {
					this.playerFoundPowerup(bodyA.getUserData());
					this.destroyed_bodies.add(bodyA);
				}
			}
			if (foundCollision) {
				this.pickupSound.play();
				this.scoreManager.addToScore(savedPersonEareds);
				this.playerEarnedPoints(x, y, savedPersonEareds);
			}
		}
		
		//uncomment this after crowd manager is sorted out
		if (this.trainYard.isBodyTrain(bodyA)) {
			boolean foundCollision = false;
			float x = 0.0f;
			float y = 0.0f;
			int points = 0;
			if (this.crowdManager.isBodyInCrowds(bodyB) && !this.destroyed_bodies.contains(bodyB, true)) {
				//System.out.println("Train: "+bodyA.toString()+ " hit bodyB: " + bodyB.toString());
				this.destroyed_bodies.add(bodyB);
				foundCollision = true;
				x = bodyB.getPosition().x;
				y = bodyB.getPosition().y;
				points = deadPersonLost;
			} else if (this.character != null && bodyB == this.character.getCharacterBody() && !this.destroyed_bodies.contains(this.character.getCharacterBody(), true)) {
				this.explosionSound.play();
				this.destroyed_bodies.add(bodyB);
				this.gameOver();
			}
			if (foundCollision) {
				
				this.hitSound.play();
				
				this.scoreManager.subtrackFromScore(10);
				this.playerLostPoints(x, y, points);
			}
		} else if (this.trainYard.isBodyTrain(bodyB)) {
			boolean foundCollision = false;
			float x = 0.0f;
			float y = 0.0f;
			int points = 0;
			if (this.crowdManager.isBodyInCrowds(bodyA) && !this.destroyed_bodies.contains(bodyA, true)) {
				this.destroyed_bodies.add(bodyA);
				foundCollision = true;
				x = bodyA.getPosition().x;
				y = bodyA.getPosition().y;
				points = deadPersonLost;
			} else if (this.character != null && bodyA == this.character.getCharacterBody() && !this.destroyed_bodies.contains(this.character.getCharacterBody(), true)) {
				this.explosionSound.play();
				this.destroyed_bodies.add(bodyA);
				this.gameOver();
			}	
			
			if (foundCollision) {
				
				this.hitSound.play();
				
				this.scoreManager.subtrackFromScore(points);
				this.playerLostPoints(x, y, points);
			}
		}	
	}
	
	public void playerFoundPowerup(Object powerup) {
		this.powerupSound.play();
		
		//do something with the powerup
		if (powerup instanceof SpeedBoostPowerup) {
			this.character.setSpeedModifier(((SpeedBoostPowerup)powerup).getSpeedBoost());
		} else if (powerup instanceof PointBoostPowerup) {
			this.character.setPointModifier(((PointBoostPowerup)powerup).getPointModifier());
		}
	}
	
	public void gameOver() {
		this.isGameOver = true;
	}
	
	public void playerEarnedPoints(float x, float y, int points) {
		Label lbl = new Label(Integer.toString(points),new LabelStyle(new BitmapFont(), Color.GREEN));
		this.stage.addActor(lbl);
		lbl.x = x;
		lbl.y = y;
		Sequence sequence = Sequence.$(
	             MoveBy.$(0, 20, 0.2f), //move actor to 200,200
	             FadeOut.$(0.5f), //fade out actor (change alpha to 0)
	             Remove.$() //remove actor from stage
				);
		lbl.action(sequence);
	}
	
	public void playerLostPoints (float x, float y, int points) {
		Label lbl = new Label("-"+Integer.toString(points),new LabelStyle(new BitmapFont(), Color.RED));
		this.stage.addActor(lbl);
		lbl.x = x;
		lbl.y = y;
		Sequence sequence = Sequence.$(
	             MoveBy.$(0, 20, 0.2f), //move actor to 200,200
	             FadeOut.$(0.5f), //fade out actor (change alpha to 0)
	             Remove.$() //remove actor from stage
				);
		lbl.action(sequence);
	}
}